/**
 * 
 */
package it.unitn.testbed.server.serial;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;

import net.tinyos.message.Message;
import net.tinyos.message.MessageListener;
import net.tinyos.message.MoteIF;
import net.tinyos.packet.BuildSource;
import net.tinyos.packet.PacketSource;
import net.tinyos.packet.PhoenixError;
import net.tinyos.packet.PhoenixSource;
import net.tinyos.util.Messenger;

import org.apache.log4j.Logger;

/**
 * Receives messages from the serial ports of the testbed motes and also handles
 * task termination.
 * 
 * You must pass to the constructor {@link #SerialReceiver(ArrayList)} a list of
 * classes that denote the serial messages that this receiver should handle.
 * There should be only one instance of this class per task. For every mote that
 * you want to receive packets from, you must call
 * {@link #connectToMote(String)}.
 * 
 * Consumers of the messages received on the serial must implement
 * {@link MessageConsumer} and must register using the
 * {@link #registerMessageConsumer(MessageConsumer)} method.
 * 
 * @author Stefan Guna
 */
public class SerialReceiver implements Messenger, MessageListener, PhoenixError {

	public static Logger log = Logger.getLogger(SerialReceiver.class.getName());
	private ArrayList<MessageConsumer> messageConsumers;
	private ArrayList<PhoenixSource> phoenixSources;
	private HashMap<Class<?>, FieldType[]> serialMsgClasses;

	/**
	 * Builds a new instance configured to receive the given type of messages.
	 * 
	 * @param serialMsgClasses
	 *            The messages to receive by this serial receiver.
	 */
	public SerialReceiver(ArrayList<Class<?>> serialMsgClasses) {
		phoenixSources = new ArrayList<PhoenixSource>();
		messageConsumers = new ArrayList<MessageConsumer>();
		this.serialMsgClasses = new HashMap<Class<?>, FieldType[]>();

		for (Class<?> msgClass : serialMsgClasses)
			this.serialMsgClasses.put(msgClass, getMessageFormat(msgClass));
	}

	/**
	 * Connects the serial receiver to the given packet source.
	 * 
	 * @param serialFw
	 *            The socket address ("host:port") of the serial forwarder used
	 *            as packet source.
	 */
	public void connectToMote(String serialFw) {
		if (serialMsgClasses == null || serialMsgClasses.size() == 0) {
			log.warn("No serial message classes found.");
			return;
		}

		PacketSource packetSource = BuildSource.makeArgsSF(serialFw);
		PhoenixSource phoenix = BuildSource.makePhoenix(packetSource, this);
		phoenix.setPacketErrorHandler(this);

		phoenixSources.add(phoenix);

		MoteIF mote = new MoteIF(phoenix);

		for (Class<?> serialMsgClass : serialMsgClasses.keySet()) {
			try {
				// TODO make sure constructor doesn't take forever
				Message msg = (Message) serialMsgClass.newInstance();
				mote.registerListener(msg, this);
			} catch (Exception e) {
				log.warn("Unable to instantiate message " + e.getMessage());
			}
		}
	}

	/**
	 * Consumes a message received on the serial port.
	 * 
	 * @param moteId
	 *            The mote sending this message.
	 * @param msg
	 *            The actual message object.
	 * @param msgClass
	 *            The type (class) of the message.
	 * @param msgCons
	 *            The consumer for this message.
	 */
	private void consumeMessage(int moteId, Message msg, Class<?> msgClass,
			MessageConsumer msgCons) {
		Object context = msgCons.beginMessage(moteId, msgClass.getName());

		Method methods[] = msgClass.getDeclaredMethods();
		for (int i = 0; i < methods.length; i++) {
			Method method = methods[i];
			int mod = method.getModifiers();

			if (!Modifier.isPublic(mod) || Modifier.isStatic(mod)
					|| Modifier.isAbstract(mod))
				continue;

			if (!method.getName().startsWith("get_")
					|| method.getParameterTypes().length != 0)
				continue;

			String fieldName = method.getName().substring(4);
			Object fieldValue;
			try {
				fieldValue = method.invoke(msg);
			} catch (Exception e) {
				log.warn("Unable to get field value: " + e.getMessage());
				continue;
			}
			msgCons.field(context, fieldName, fieldValue);
		}

		msgCons.endMessage(context);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.tinyos.packet.PhoenixError#error(java.io.IOException)
	 */
	public void error(IOException ioe) {
		log.warn(ioe.getMessage());
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			log.warn(e.getMessage());
		}
	}

	/**
	 * Gets the type of a message field.
	 * 
	 * @param msgClass
	 *            The class denoting the message.
	 * @param method
	 *            The "isArray_" method of the class.
	 * @return The field type as desribed by {@link FieldType}.
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 * @throws InvocationTargetException
	 * @throws SecurityException
	 * @throws NoSuchMethodException
	 */
	private FieldType getFieldType(Class<?> msgClass, Method method)
			throws IllegalArgumentException, IllegalAccessException,
			InvocationTargetException, SecurityException, NoSuchMethodException {

		String fieldName = method.getName().substring(8);
		Boolean isArray = (Boolean) method.invoke(null);

		if (!isArray) {
			Method getter = msgClass.getMethod("get_" + fieldName);
			if (getter == null) {
				log.warn("  getter not found for field " + fieldName);
				return null;
			}
			Class<?> type = getter.getReturnType();
			return new FieldType(type, fieldName, 1);
		}

		Method fieldCountMethod = msgClass
				.getMethod("numElements_" + fieldName);
		if (fieldCountMethod == null
				|| fieldCountMethod.getReturnType() != Integer.TYPE) {
			log.warn("  numElements not found for field " + fieldName);
			return null;
		}

		int modC = fieldCountMethod.getModifiers();
		if (!(Modifier.isPublic(modC) && Modifier.isStatic(modC)
				& !Modifier.isAbstract(modC)))
			return null;

		Integer fieldCount = (Integer) fieldCountMethod.invoke(null);
		Method elementGetter = msgClass.getMethod("getElement_" + fieldName,
				Integer.TYPE);
		if (elementGetter == null) {
			log.warn("  element getter not found for field " + fieldName);
			return null;
		}
		Class<?> type = elementGetter.getReturnType();
		return new FieldType(type, fieldName, fieldCount);
	}

	/**
	 * Gets the format of a serial message.
	 * 
	 * @param msgClass
	 *            The class of the serial message.
	 */
	private FieldType[] getMessageFormat(Class<?> msgClass) {
		log.debug("Building descriptor for " + msgClass.getCanonicalName());
		ArrayList<FieldType> result = new ArrayList<FieldType>();

		Method[] methods = msgClass.getDeclaredMethods();

		for (int i = 0; i < methods.length; i++) {
			Method method = methods[i];
			int mod = method.getModifiers();

			log.debug("  found method: " + method);

			if (!(Modifier.isPublic(mod) && Modifier.isStatic(mod)
					& !Modifier.isAbstract(mod)))
				continue;

			if (method.getParameterTypes().length != 0
					|| method.getReturnType() != Boolean.TYPE)
				continue;

			if (!method.getName().startsWith("isArray_"))
				continue;

			FieldType fieldType;
			try {
				fieldType = getFieldType(msgClass, method);
			} catch (Exception e) {
				log.warn(e.getMessage());
				continue;
			}
			log.debug("  found field: " + fieldType);
			result.add(fieldType);
		}
		return (FieldType[]) result.toArray();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.tinyos.util.Messenger#message(java.lang.String)
	 */
	public void message(String arg0) {
		log.debug(arg0);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.tinyos.message.MessageListener#messageReceived(int,
	 * net.tinyos.message.Message)
	 */
	public void messageReceived(int moteId, Message msg) {
		if (serialMsgClasses == null) {
			// TODO set a rate for displaying this log
			log.warn("Message received, but no messages defined.");
			return;
		}

		Class<?> msgClass = null;
		for (Class<?> tmp : serialMsgClasses.keySet())
			// TODO compare amType
			// TODO have mapping (messages, node)
			if (tmp.isInstance(msg)) {
				msgClass = tmp;
				break;
			}
		if (msgClass == null) {
			log.debug("Invalid message received, ignoring it.");
			return;
		}

		log.trace("Msg of type " + msgClass.getCanonicalName()
				+ " received from " + moteId + ":");

		for (MessageConsumer msgCons : messageConsumers)
			consumeMessage(moteId, msg, msgClass, msgCons);
	}

	/**
	 * Registers a new message consumer.
	 * 
	 * @param msgCons
	 *            The message consumer to register.
	 */
	public void registerMessageConsumer(MessageConsumer msgCons) {
		messageConsumers.add(msgCons);
		for (Class<?> msgClass : serialMsgClasses.keySet())
			msgCons.newMessageType(msgClass.getName(), serialMsgClasses
					.get(msgClass));
	}

	/** Terminates all serial forwarder connections. */
	public void terminate() {
		if (serialMsgClasses == null || serialMsgClasses.size() == 0)
			return;

		for (PhoenixSource phoenix : phoenixSources)
			phoenix.shutdown();
	}

	/**
	 * Unregisters a message consumer.
	 * 
	 * @param msgCons
	 *            The message consumer to unregister.
	 */
	public void unregisterMessageConsumer(MessageConsumer msgCons) {
		messageConsumers.remove(msgCons);
	}
}
